import java.net.ServerSocket;
import java.net.Socket;
import java.net.InetAddress;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.ArrayList;

/**
 * This class is used to listen for incoming requests
 * from clients, choose a appropriate way to handle them,
 * and send back responses depending on the request sent.
 *
 * @author Marlon Etheredge
 * @author <a href="mailto:m.etheredge@gmail.com">m.etheredge@gmail.com</a>
 * @author <a href="http://www.foxage.nl/">http://www.foxage.nl</a>
 */
public class Webserver extends Thread {
	
	/** ServerSocket for clients to connect to this Webserver instance */
	private ServerSocket serverSocket;
	
	/** Wether this Webserver instance should 'run' or 'listen' by default, this Webserver is listening */
	private boolean running = true;
	
	/** Default port for use with this Webserver */
	private int defaultPort = 80;
	
	/** Port used for this Webserver instance */
	private int port;
	
	/** ArrayList containing all Sockets (connections) serving as a qeue */
	private ArrayList<Socket> socketQeue = new ArrayList<Socket>();
	
	/** ArrayList containing all ConnectionHandler for this instance of Webserver */
	private ArrayList<ConnectionHandler> childHandlers = new ArrayList<ConnectionHandler>();
	
	/** Bind address for this Webserver. */
	private InetAddress bindAddress;

	/**
	 * Returns the bindAddress.
	 *
	 * @return InetAddress The BindAddress
	 */
	public InetAddress getBindAddress() {
		return this.bindAddress;
	}

	/**
	 * Returns the port.
	 *
	 * @return int The port
	 */
	public int getPort() {
		return this.port;
	}

	/**
	 * Constructs a new Webserver instance, creates a ServerSocket 
	 * and starts this Webserver Thread. This Webserver runs on port specified
	 * by serverPort parameter.
	 *
	 * @param serverPort Port used for this Webserver
	 */
	public Webserver(InetAddress bindAddress, int serverPort) {
		this.port = serverPort;
		this.bindAddress = bindAddress;	

		this.initializeWebserver();
		
		Webster.log(Level.INFO, "Webserver running on port " + this.port);
		
		this.start();
	}
	
	/**
	 * Initializes this Webserver instance, creates a ServerSocket on a given,
	 * default, or in case of a conflicting port number an incremented given or default
	 * portnumber will be used.
	 */
	public void initializeWebserver() {
		boolean correctPort = false;
		
		while(!correctPort) {
			try {
				if(this.bindAddress != null) {
					this.serverSocket = new ServerSocket();
					this.serverSocket.bind(new InetSocketAddress(this.bindAddress, this.port));
				} else
					this.serverSocket = new ServerSocket(this.port);

				correctPort = true;
			} catch(BindException exception) {
				Webster.log(Level.INFO, "Portnumber " + this.port + " is already in use, " +
							"retrying with port " + (++this.port));
			} catch(IOException exception) {
				Webster.log(Level.SEVERE, "IOException thrown at Webserver.initializeWebserver(), " + 
							  "could not create ServerSocket, this Webserver (port " + this.port + ")  thread will now die");
			}
		}
	}
	
	/**
	 * Run method for this Webserver Thread, while running flag
	 * is true, this will listen.
	 */
	public void run() {
		for(int n = 0 ; n < Integer.parseInt(Webster.getProperty("maxhandlers")) ; n++) {
			this.newConnectionHandler();
		}

		if(Webster.verbose)
			Webster.log(Level.INFO, "Running " + this.childHandlers.size() + " handlers, requested handlers: " + Webster.getProperty("maxhandlers"));

		while(this.running) {
			Socket socket = null;

			if(!this.running)
				break;

			try {
				while(this.socketQeue.size() >= Integer.parseInt(Webster.getProperty("maxconnections"))) {
					try {	
						Thread.sleep(100);
					} catch(InterruptedException exception) {
						//Do nothing?
					}
				}

				socket = this.serverSocket.accept();
				
				if(Webster.verbose)
					Webster.log(Level.INFO, "New connection \"" + socket + "\"");
			} catch(SocketTimeoutException exception) {
				Webster.log(Level.SEVERE, "SocketTimeOutException thrown at Webserver.run(), please check " +
							  "this internet connection, " +
							  "this Webserver (port " + this.port + ") thread will now die.");
				
				Webster.removeWebserver(this);
			} catch(IOException exception) {
				Webster.log(Level.SEVERE, "IOException thrown at Webserver.run(), " +
							  "this Webserver (port " + this.port + ") thread will now die.");

				Webster.removeWebserver(this);
			}
			
			this.socketQeue.add(socket);
			
			synchronized(this.socketQeue) {
				this.socketQeue.notifyAll();
			}
		}
	}
	
	/**
	 * Puts this Webserver instance in listen modus,
	 * to listen for request from clients.
	 */
	public void startListen() {
		this.running = true;
	}
	
	/**
	 * Returns the next Socket in line of the qeue,
	 * and removes it from the SocketQeue.
	 *
	 * @return Next Socket in qeue
	 */
	public Socket getNextSocket() {
		synchronized(this.socketQeue) {
			Socket nextSocket;

			while(this.socketQeue.isEmpty()) {
				try {
					this.socketQeue.wait();
				} catch(InterruptedException exception) {
					//Do nothing?
				} catch(IllegalMonitorStateException exception) {
					//This should never happen, if it does
					//it will be serious trouble.
					Webster.log(Level.WARNING, "IllegalMonitorStateException thrown at Webserver.getNextSocket(), " +
								   "this is a serious error, and this Webserver (port " + this.port + ") " +
								   "will now die.");
											
					Webster.removeWebserver(this);
				}
			}
		
			try {
				nextSocket = socketQeue.get(0);
				this.socketQeue.remove(nextSocket);
			} catch(IndexOutOfBoundsException exception) {
				Webster.log(Level.WARNING, "IndexOutOfBoundException thrown at Webserver.getNextSocket(), " +
							   "this could mean that the next client in qeue cannot be handled, " +
					     		   "null was returned for Webserver (port " + this.port + ").");
										
				return null;
			}
			
			if(Webster.verbose)
				Webster.log(Level.INFO, "Next Socket in line \"" + nextSocket + "\"");

			return nextSocket;
		}
	}
	
	/**
	 * Stops listening modus on the Webserver instance.
	 */
	public void stopListen() {
		this.running = false;
		
		Webster.log(Level.INFO, "Webserver \"" + this + "\" stopped listening");
		
		for(ConnectionHandler connectionHandler : this.childHandlers) {
			connectionHandler.stopRunning();
		}
		
	}
	
	/** Sets the DefaultPort */
	private void setDefaultPort(int defaultPort) {
		this.defaultPort = defaultPort;
	}
	
	/** Creates a new ConnectionHandler instance, and adds it to childHandlers */
	private void newConnectionHandler() {
		ConnectionHandler newHandler = new ConnectionHandler(this);
		childHandlers.add(newHandler);
	}
	
	/**
	 * Removes ConnectionHandler specified 
	 * from the childHandlers ArrayList, and stops
	 * it's Thread.
	 *
	 * @param connectionHandler ConnectionHandler to remove
	 */
	public void removeConnectionHandler(ConnectionHandler connectionHandler) {
		if(Webster.verbose)
			Webster.log(Level.INFO, "Removing ConnectionHandler \"" + connectionHandler + "\"");
		
		connectionHandler.stopRunning();
		this.childHandlers.remove(connectionHandler);
	}
	
	/** Returns a String representation of this Webserver instance */
	@Override
	public String toString() {
		return this.bindAddress + ":" + this.port;
	}

	/**
	 * Finalize method, in this case cleans up the Socket.
	 */
	protected void finalize() throws Throwable {
		try {	
			this.serverSocket.close();
		} catch(IOException exception) {
			Webster.log(Level.WARNING, "Could not finalize webserver: " + this); //If this happens, %%@#$!
		}

		for(Socket socket : this.socketQeue) {
			try {
				socket.close();
			} catch(IOException exception) {
				Webster.log(Level.WARNING, "Could not finalize webserver: " + this + ", socket " + socket +
							   "could not be closed."); //If this happens, %%@#$!
			}
		}
	}
	
}
